/**
 * \file
 */
#include "precompiled.hpp"
#include "kfs_file_descriptor.hpp"
#include "kfs_impl.hpp"
#include "kfs_cache.hpp"
#include "kfs_time.hpp"
#include "Exception.hpp"
#include <errno.h>            // EINVAL et al
#include <fcntl.h>            // O_CREAT et al
#include <inttypes.h>         // PRIu64
#include <iterator>
#include <memory>
#include <string>
#include <sys/stat.h>         // S_ISDIR et al
#include <unistd.h>           // mode_t, uid_t, gid_t

using boost::scoped_array;
using std::auto_ptr;  using std::distance;  using std::min;
using std::string;  using std::vector;


FileDescriptor*
kfs::file_create(string const& path, int flags, mode_t mode, uid_t uid,
    gid_t gid)
{
    /// The parent directory always has a trailing slash.
    string parentPath;
    /// The child name has no slashes.
    string childName;
    kfs_path_split(path, parentPath, childName);

    /* Open the parent directory in read-write mode. This is necessary to
     * add the entry to the directory's internal structure.
     */
    int const parentFlags = O_RDWR;
    boost::scoped_ptr<FileDescriptor> parent(file_open(
        parentPath.c_str(), parentFlags));
    printf("%s: create: parent: {name: %s inode: %u}\n",
        __FUNCTION__, parentPath.c_str(),
        parent->inodeRef->elt->second->blockNum);

    return impl->inode_create(parent.get(), this, path, childName, flags,
        mode, uid, gid);
}


FileDescriptor*
kfs::file_open(string const& path, int flags)
{
    /* /usr/include/bits/fcntl.h:
     * open/fcntl - O_SYNC is only implemented on blocks devices and on files
     * located on a few file systems.
        #define O_ACCMODE          0003
        #define O_RDONLY             00
        #define O_WRONLY             01
        #define O_RDWR               02
        #define O_CREAT            0100 // not fcntl
        #define O_EXCL             0200 // not fcntl
        #define O_NOCTTY           0400 // not fcntl
        #define O_TRUNC           01000 // not fcntl
        #define O_APPEND          02000
        #define O_NONBLOCK        04000
        #define O_NDELAY        O_NONBLOCK
        #define O_SYNC         04010000
        #define O_FSYNC          O_SYNC
        #define O_ASYNC          020000
     */
    if(flags & O_EXCL)
        throw Error(ENOSYS, "O_EXCL not implemented");

    std::auto_ptr<FileDescriptor> file(new FileDescriptor(this, flags));
    try
    {
        file->inodeRef.reset(impl->cache_get(path));
    }
    catch(Error& e)
    {
        if((flags & O_CREAT) && e.error() == ENOENT)
        {
            uid_t uid = 0; gid_t gid = 0; mode_t mode = 0;
            return file_create(path, flags, mode, uid, gid);
        }
        else
        {
            throw e;
        }
    }
    if(flags & O_TRUNC)
    {
        if((flags & O_ACCMODE) == O_RDONLY)
            throw Error(EACCES, "Cannot truncate a file opened in read-only "
                "mode");
        file->truncate(0);
    }
    return file.release();
}


FileDescriptor*
kfs::file_open(block_index_t const& inodeBlockNum,
    string const& path, int flags)
{
    /* /usr/include/bits/fcntl.h:
     * open/fcntl - O_SYNC is only implemented on blocks devices and on files
     * located on a few file systems.
        #define O_ACCMODE          0003
        #define O_RDONLY             00
        #define O_WRONLY             01
        #define O_RDWR               02
        #define O_CREAT            0100 // not fcntl
        #define O_EXCL             0200 // not fcntl
        #define O_NOCTTY           0400 // not fcntl
        #define O_TRUNC           01000 // not fcntl
        #define O_APPEND          02000
        #define O_NONBLOCK        04000
        #define O_NDELAY        O_NONBLOCK
        #define O_SYNC         04010000
        #define O_FSYNC          O_SYNC
        #define O_ASYNC          020000
     */
    if(flags & O_EXCL)
        throw Error(ENOSYS, "O_EXCL is not implemented");
    if(flags & O_CREAT)
        throw Error(EINVAL, "O_CREAT is not valid");

    std::auto_ptr<FileDescriptor> file(new FileDescriptor(this, flags));
    file->inodeRef.reset(impl->cache_get_from_disk(inodeBlockNum, path));
    if(flags & O_TRUNC)
    {
        if((flags & O_ACCMODE) == O_RDONLY)
            throw Error(EACCES, "Cannot truncate a file opened in read-only "
                "mode");
        file->truncate(0);
    }
    return file.release();
}


void
kfs::file_close(FileDescriptor* const file)
{
    delete file;
}

//
//-----------------------------------------------------------------------------
//


FileDescriptor::FileDescriptor(kfs* fs, int flags)
: flags(flags), fs(fs), inodeRef()
{
}


FileDescriptor::~FileDescriptor()
{
    // CacheRef is deleted.
}


void
FileDescriptor::chmod(mode_t mode)
{
    CacheInode& inodeNode = *inodeRef->elt->second;
    Inode& inode = inodeNode.inode;
    inode.mode &= ~07777;
    inode.mode |= (mode & 07777);
    inodeRef->write();
}


void
FileDescriptor::chown(uid_t uid, gid_t gid)
{
    CacheInode& inodeNode = *inodeRef->elt->second;
    Inode& inode = inodeNode.inode;
    // If the owner or group is specified as -1, then that ID is not
    // changed.
    if((int) uid != -1)
        inode.userID = uid;
    if((int) gid != -1)
        inode.groupID = gid;
    inodeRef->write();
}


void
FileDescriptor::getattr(struct stat* mystat) const
{
    CacheInode const& inodeNode = *inodeRef->elt->second;
    Inode const& inode = inodeNode.inode;
    memset(mystat, 0, sizeof(struct stat));

    // Convert size to 512-byte units.
    mystat->st_blocks = (fs->impl->fileNumBlocks(inode) * 512)
        / fs->impl->mb.blockNumBytes;
    mystat->st_blksize = fs->impl->mb.blockNumBytes;
    mystat->st_ino = inodeNode.blockNum;
    mystat->st_nlink = inode.linkCount;
    mystat->st_mode = inode.mode;
    mystat->st_size = inode.sizeNumBytes;
    mystat->st_uid = inode.userID;
    mystat->st_gid = inode.groupID;
    #ifdef COMPLIANT
    mystat->st_atime = (time_t)(inode.atime / 1000000000);
    mystat->st_mtime = (time_t)(inode.mtime / 1000000000);
    mystat->st_ctime = (time_t)(inode.ctime / 1000000000);
    #else
    mystat->st_atim.tv_sec  = (time_t)(inode.atime / 1000000000);
    mystat->st_atim.tv_nsec = (time_t)(inode.atime % 1000000000);
    mystat->st_mtim.tv_sec  = (time_t)(inode.mtime / 1000000000);
    mystat->st_mtim.tv_nsec = (time_t)(inode.mtime % 1000000000);
    mystat->st_ctim.tv_sec  = (time_t)(inode.ctime / 1000000000);
    mystat->st_ctim.tv_nsec = (time_t)(inode.ctime % 1000000000);
    #endif
}


size_t
FileDescriptor::read(void* const destinationBeginVoid, size_t maxNumBytes,
    file_size_t fileStartPosition)
const
{
    // TODO Check ((flags & O_ACCMODE) != O_WRONLY)
    size_t const blockNumBytes = (size_t) fs->impl->mb.blockNumBytes;
    block_index_t fileBlockNum = (block_index_t)
        (fileStartPosition / blockNumBytes);
    block_index_t const fileNumBlocks_ =
        fs->impl->fileNumBlocks(inodeRef->elt->second->inode);
    file_size_t const& fileSizeBytes =
        inodeRef->elt->second->inode.sizeNumBytes;
    size_t offsetInBlock = (size_t)(fileStartPosition % blockNumBytes);
    char* destinationBegin = static_cast<char*>(destinationBeginVoid);
    char* destination = destinationBegin;
    char* const destinationEnd = destinationBegin + maxNumBytes;
    scoped_array<uint8_t> block(fs->impl->createBlockBuffer());

    for( ; destination < destinationEnd; ++fileBlockNum)
    {
        size_t numBytes;
        uint8_t* source = block.get();
        size_t destBytesRemain = (size_t)(destinationEnd - destination);
        size_t thisBlockNumBytes;
        if(fileBlockNum == fileNumBlocks_ - 1 &&
           (size_t)fileSizeBytes % (size_t)blockNumBytes )
        {
            thisBlockNumBytes = (size_t)( fileSizeBytes %
                blockNumBytes);
        }
        else
        {
            thisBlockNumBytes = blockNumBytes;
        }

        if(offsetInBlock >= thisBlockNumBytes)
        {
            break;
        }
        else if(offsetInBlock != 0)
        {
            // Read from the middle of the block (only for the first block).
            source += offsetInBlock;
            numBytes = min(
                thisBlockNumBytes - offsetInBlock,
                destBytesRemain
            );
            offsetInBlock = 0;
        }
        else
        {
            // Read from the beginning of this block.
            numBytes = min(thisBlockNumBytes, destBytesRemain);
        }

        try
        {
            // Pull the entire block into memory.
            block_index_t diskBlockNum = fs->impl->file_resolveDiskBlock(
                *inodeRef, fileBlockNum);
            kfs_disk_read(*fs->impl, diskBlockNum, block.get());
            // Copy part of the block into the caller's buffer.
            memcpy(destination, source, numBytes);
            destination += numBytes;
        }
        catch(Exception& e)
        {
            fprintf(stderr,"%s: inode %u: %s\n", __FUNCTION__,
                inodeRef->elt->second->blockNum, e.what());
            break;
        }
    }
    size_t numBytesRead = destination - destinationBegin;
    return numBytesRead;
}


void FileDescriptor::truncate(file_size_t const newSize)
{
    typedef vector<block_index_t> BlockList;
    block_off_t const blockNumBytes = (block_off_t) fs->impl->mb.blockNumBytes;
    Inode& inode = inodeRef->elt->second->inode;
    block_index_t const oldNumBlocks = fs->impl->fileNumBlocks(inode);
    block_index_t const newNumBlocks = div_ceil<file_size_t>(newSize, blockNumBytes);
    scoped_array<uint8_t> buffer(fs->impl->createBlockBuffer());

    if(oldNumBlocks < newNumBlocks)
    {
        // Zero out the end of the last block.
        block_off_t const offset = (block_off_t)(inode.sizeNumBytes % blockNumBytes);
        if(offset != 0)
        {
            assert(oldNumBlocks);
            block_index_t blkid =  fs->impl->file_resolveDiskBlock(*inodeRef,
                oldNumBlocks - 1);
            kfs_disk_read(*fs->impl, blkid, buffer.get());
            memset(buffer.get() + offset, 0, blockNumBytes - offset);
            kfs_disk_write(*fs->impl, blkid, buffer.get());
        }
        // Add more blocks to the file.
        file_size_t growNumBytes = newSize - inode.sizeNumBytes;
        BlockList diskBlocks;
        fs->impl->file_allocator_grow(*inodeRef, growNumBytes, &diskBlocks);
        // Zero out the new blocks.
        memset(buffer.get(), 0, fs->impl->mb.blockNumBytes);
        for(BlockList::const_iterator bid = diskBlocks.begin();
            bid != diskBlocks.end(); ++bid)
        {
            kfs_disk_write(*fs->impl, *bid, buffer.get());
        }
    }
    else if( oldNumBlocks > newNumBlocks )
    {
        // Remove blocks from the file.
        block_index_t const removeNumBlocks = oldNumBlocks - newNumBlocks;
        for(block_index_t i = 0; i < removeNumBlocks; ++i)
        {
            fs->impl->file_allocator_shrink(*inodeRef);
        }
        // Zero out the end of the last block.
        block_off_t const offset = (block_off_t)(newSize % blockNumBytes);
        if(offset != 0)
        {
            assert(newNumBlocks);
            block_index_t blkid =  fs->impl->file_resolveDiskBlock(*inodeRef,
                newNumBlocks - 1);
            kfs_disk_read(*fs->impl, blkid, buffer.get());
            memset(buffer.get() + offset, 0, blockNumBytes - offset);
            kfs_disk_write(*fs->impl, blkid, buffer.get());
        }
        // Alter the "file size" field in the inode.
        if(inodeRef->elt->second->inode.sizeNumBytes != newSize)
        {
            inodeRef->elt->second->inode.sizeNumBytes = newSize;
            inodeRef->write();
        }
    }
    else // Do not change the number of blocks.
    {
        // Zero out the end of the last block.
        file_size_t const oldSize = inode.sizeNumBytes;
        // Alter the "file size" field in the inode.
        if(newSize != oldSize)
        {
            if(newSize > oldSize)
            {
                // Zero out the end of the last block.
                block_off_t const offset = (block_off_t)(oldSize % blockNumBytes);
                if(offset != 0)
                {
                    assert(oldNumBlocks);
                    block_index_t blkid =  fs->impl->file_resolveDiskBlock(*inodeRef,
                        oldNumBlocks - 1);
                    kfs_disk_read(*fs->impl, blkid, buffer.get());
                    memset(buffer.get() + offset, 0, blockNumBytes - offset);
                    kfs_disk_write(*fs->impl, blkid, buffer.get());
                }
            }
            // Update the file size in the inode.
            inode.sizeNumBytes = newSize;
            inodeRef->write();
        }
    }

    if(fs->impl->fileNumBlocks(inode) != newNumBlocks)
    {
        throw Exception("%s: file_allocator did not do what we expected",
            __FUNCTION__);
    }
}


size_t
FileDescriptor::write(void const* sourceBeginVoid, size_t maxNumBytes,
    file_size_t fileStartPosition)
{
    // What to do here? In tests, fuse would not get this far if the file was
    // opened for read-only access.
    if((flags & O_ACCMODE) == O_RDONLY)
        throw Error(EACCES, 0);

    size_t const blockNumBytes = (size_t) fs->impl->mb.blockNumBytes;
    // Compute starting block number and offset within that block.
    block_index_t fileBlockNum = static_cast<block_index_t>(
        fileStartPosition / blockNumBytes);
    CacheInode& inodeNode = *inodeRef->elt->second;
    size_t offsetInBlock = (size_t)(fileStartPosition % blockNumBytes);
    char const* sourceBegin = static_cast<char const*>(sourceBeginVoid);
    char const* source = sourceBegin;
    char const* const sourceEnd = sourceBegin + maxNumBytes;
    scoped_array<uint8_t> block(fs->impl->createBlockBuffer());

    for( ; source < sourceEnd; ++fileBlockNum)
    {
        size_t numBytes;
        uint8_t* destination = block.get();
        size_t sourceBytesRemain = (size_t)(sourceEnd - source);
        if(offsetInBlock != 0)
        {
            // Write into the middle of the first block.
            destination += offsetInBlock;
            numBytes = min(
                blockNumBytes - offsetInBlock,
                sourceBytesRemain
            );
        }
        else
        {
            // Write into the beginning of the block.
            numBytes = min(blockNumBytes, sourceBytesRemain);
        }
        try
        {
            if(fileBlockNum == fs->impl->fileNumBlocks(inodeNode.inode))
            {
                // Append one block.
                vector<block_index_t> diskBlockNums;
                fs->impl->file_allocator_grow(*inodeRef, numBytes, &diskBlockNums);
                // Write the data into the new block.
                memcpy(block.get(), source, numBytes);
                // Zero out the end of the data block.
                memset(block.get() + numBytes, 0, blockNumBytes - numBytes);
                kfs_disk_write(*fs->impl, diskBlockNums[0], block.get());
            }
            else
            {
                // Overwrite one block.
                block_index_t diskBlockNum = fs->impl->file_resolveDiskBlock(
                    *inodeRef, fileBlockNum);
                if(diskBlockNum == 0)
                    //Yikes
                    break;

                if(numBytes != blockNumBytes)
                {
                    // Some old data in this block is preserved.
                    kfs_disk_read(*fs->impl, diskBlockNum, block.get());
                }
                memcpy(destination, source, numBytes);
                kfs_disk_write(*fs->impl, diskBlockNum, block.get());
                if(fileBlockNum == fs->impl->fileNumBlocks(inodeNode.inode) - 1)
                {
                    // We wrote to the file's final block.  Did the file size
                    // grow?
                    file_size_t newSize = fileBlockNum * blockNumBytes
                        + offsetInBlock + numBytes;
                    if(newSize > inodeNode.inode.sizeNumBytes)
                    {
                        inodeNode.inode.sizeNumBytes = newSize;
                        inodeRef->write();
                    }
                }
            }
            source += numBytes;
            offsetInBlock = 0;
        }
        catch(Exception& e)
        {
            fprintf(stderr,"%s: file %u: %s\n", __FUNCTION__,
                inodeNode.blockNum, e.what());
            break;
        }
    }

    size_t numBytesWritten = (size_t)(source - sourceBegin);
    if(numBytesWritten)
    {
        inodeNode.inode.mtime = KfsTime::timeNow();
        inodeNode.inode.atime = inodeNode.inode.mtime;
        inodeRef->write();
    }
    return (numBytesWritten == maxNumBytes) ? maxNumBytes : 0;
}

//
//-----------------------------------------------------------------------------
//


FileDescriptor*
kfs::Impl::fd_deref(uint64_t fdNumber)
{
    if(fdNumber < 2)
    {
        throw Error(EINVAL,"Bad file descriptor %"PRIu64, fdNumber);
    }

    FileDescriptor* file = reinterpret_cast<FileDescriptor*>(fdNumber);
    return file;
}


void
kfs::Impl::fd_release(uint64_t fdNumber)
{
    if(fdNumber < 2)
    {
        throw Error(EINVAL,"Bad file descriptor %"PRIu64, fdNumber);
    }

    FileDescriptor* file = reinterpret_cast<FileDescriptor*>(fdNumber);
    delete file;
}
